/****************************************************************************
 * SSHFS Mutiplex Filesystem Cache Subsystem
 * Copyright (C) 2008,2009,2010  Nan Dun <dunnan@yl.is.s.u-tokyo.ac.jp>
 * Department of Computer Science, The University of Tokyo
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * This program can be distributed under the terms of the GNU GPL.
 * See the file COPYING.
 ***************************************************************************/

#include "cache.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include <pthread.h>

#define DEFAULT_CACHE_TIMEOUT 20
#define MAX_CACHE_SIZE 10000
#define MIN_CACHE_CLEAN_INTERVAL 5
#define CACHE_CLEAN_INTERVAL 60

struct cache {
	int on;
	unsigned stat_timeout;
	unsigned dir_timeout;
	unsigned link_timeout;
	struct fuse_cache_operations *next_oper;
	GHashTable *table;
	pthread_mutex_t lock;
	time_t last_cleaned;
	uint64_t write_ctr;
};

static struct cache cache;

struct node {
	struct stat stat;
	time_t stat_valid;
	char **dir;
	time_t dir_valid;
	char *link;
	time_t link_valid;
	time_t valid;
	void *data;
};

struct fuse_cache_dirhandle {
	const char *path;
	fuse_dirh_t h;
	fuse_dirfil_t filler;
	GPtrArray *dir;
	uint64_t wrctr;
};

static void free_node(gpointer node_)
{
	struct node *node = (struct node *) node_;
	g_strfreev(node->dir);
	g_free(node);
}

static int cache_clean_entry(void *key_, struct node *node, time_t *now)
{
	(void) key_;
	if (*now > node->valid)
		return TRUE;
	else
		return FALSE;
}

static void cache_clean(void)
{
	time_t now = time(NULL);
	if (now > cache.last_cleaned + MIN_CACHE_CLEAN_INTERVAL &&
	    (g_hash_table_size(cache.table) > MAX_CACHE_SIZE ||
	     now > cache.last_cleaned + CACHE_CLEAN_INTERVAL)) {
		g_hash_table_foreach_remove(cache.table,
					    (GHRFunc) cache_clean_entry, &now);
		cache.last_cleaned = now;
	}
}

static struct node *cache_lookup(const char *path)
{
	return (struct node *) g_hash_table_lookup(cache.table, path);
}

static void cache_purge(const char *path)
{
	g_hash_table_remove(cache.table, path);
}

static void cache_purge_parent(const char *path)
{
	const char *s = strrchr(path, '/');
	if (s) {
		if (s == path)
			g_hash_table_remove(cache.table, "/");
		else {
			char *parent = g_strndup(path, s - path);
			cache_purge(parent);
			g_free(parent);
		}
	}
}

void cache_invalidate(const char *path)
{
	if (!cache.on)
		return;

	pthread_mutex_lock(&cache.lock);
	cache_purge(path);
	pthread_mutex_unlock(&cache.lock);
}

void cache_invalidate_write(const char *path)
{
	pthread_mutex_lock(&cache.lock);
	cache_purge(path);
	cache.write_ctr++;
	pthread_mutex_unlock(&cache.lock);
}

static void cache_invalidate_dir(const char *path)
{
	pthread_mutex_lock(&cache.lock);
	cache_purge(path);
	cache_purge_parent(path);
	pthread_mutex_unlock(&cache.lock);
}

static int cache_del_children(const char *key, void *val_, const char *path)
{
	(void) val_;
	if (strncmp(key, path, strlen(path)) == 0)
		return TRUE;
	else
		return FALSE;
}

static void cache_do_rename(const char *from, const char *to)
{
	pthread_mutex_lock(&cache.lock);
	g_hash_table_foreach_remove(cache.table, (GHRFunc) cache_del_children,
				    (char *) from);
	cache_purge(from);
	cache_purge(to);
	cache_purge_parent(from);
	cache_purge_parent(to);
	pthread_mutex_unlock(&cache.lock);
}

static struct node *cache_get(const char *path)
{
	struct node *node = cache_lookup(path);
	if (node == NULL) {
		char *pathcopy = g_strdup(path);
		node = g_new0(struct node, 1);
		g_hash_table_insert(cache.table, pathcopy, node);
	}
	return node;
}

void cache_add_attr(const char *path, const struct stat *stbuf, uint64_t wrctr)
{
	struct node *node;
	time_t now;

	if (!cache.on)
		return;

	pthread_mutex_lock(&cache.lock);
	if (wrctr == cache.write_ctr) {
		node = cache_get(path);
		now = time(NULL);
		node->stat = *stbuf;
		node->stat_valid = time(NULL) + cache.stat_timeout;
		if (node->stat_valid > node->valid)
			node->valid = node->stat_valid;
		cache_clean();
	}
	pthread_mutex_unlock(&cache.lock);
}

static void cache_add_dir(const char *path, char **dir)
{
	struct node *node;
	time_t now;

	pthread_mutex_lock(&cache.lock);
	node = cache_get(path);
	now = time(NULL);
	g_strfreev(node->dir);
	node->dir = dir;
	node->dir_valid = time(NULL) + cache.dir_timeout;
	if (node->dir_valid > node->valid)
		node->valid = node->dir_valid;
	cache_clean();
	pthread_mutex_unlock(&cache.lock);
}

static size_t my_strnlen(const char *s, size_t maxsize)
{
	const char *p;
	for (p = s; maxsize && *p; maxsize--, p++);
	return p - s;
}

static void cache_add_link(const char *path, const char *link, size_t size)
{
	struct node *node;
	time_t now;

	pthread_mutex_lock(&cache.lock);
	node = cache_get(path);
	now = time(NULL);
	g_free(node->link);
	node->link = g_strndup(link, my_strnlen(link, size-1));
	node->link_valid = time(NULL) + cache.link_timeout;
	if (node->link_valid > node->valid)
		node->valid = node->link_valid;
	cache_clean();
	pthread_mutex_unlock(&cache.lock);
}

int cache_get_attr(const char *path, struct stat *stbuf)
{
	struct node *node;
	int err = -EAGAIN;
	if (!cache.on)
		return err;
	pthread_mutex_lock(&cache.lock);
	node = cache_lookup(path);
	if (node != NULL) {
		time_t now = time(NULL);
		if (node->stat_valid - now >= 0) {
			*stbuf = node->stat;
			err = 0;
		}
	}
	pthread_mutex_unlock(&cache.lock);
	return err;
}

uint64_t cache_get_write_ctr(void)
{
	uint64_t res;

	pthread_mutex_lock(&cache.lock);
	res = cache.write_ctr;
	pthread_mutex_unlock(&cache.lock);

	return res;
}

static int cache_getattr(const char *path, struct stat *stbuf)
{
	int err = cache_get_attr(path, stbuf);
	if (err) {
		uint64_t wrctr = cache_get_write_ctr();
		err = cache.next_oper->oper.getattr(path, stbuf);
		if (!err)
			cache_add_attr(path, stbuf, wrctr);
	}
	return err;
}

static int cache_readlink(const char *path, char *buf, size_t size)
{
	struct node *node;
	int err;

	pthread_mutex_lock(&cache.lock);
	node = cache_lookup(path);
	if (node != NULL) {
		time_t now = time(NULL);
		if (node->link_valid - now >= 0) {
			strncpy(buf, node->link, size-1);
			buf[size-1] = '\0';
			pthread_mutex_unlock(&cache.lock);
			return 0;
		}
	}
	pthread_mutex_unlock(&cache.lock);
	err = cache.next_oper->oper.readlink(path, buf, size);
	if (!err)
		cache_add_link(path, buf, size);

	return err;
}

static int cache_dirfill(fuse_cache_dirh_t ch, const char *name,
                         const struct stat *stbuf)
{
	int err = ch->filler(ch->h, name, 0, stbuf->st_ino);
	if (!err) {
		g_ptr_array_add(ch->dir, g_strdup(name));
		if (stbuf->st_mode & S_IFMT) {
			char *fullpath;
			const char *basepath = !ch->path[1] ? "" : ch->path;

			fullpath = g_strdup_printf("%s/%s", basepath, name);
			cache_add_attr(fullpath, stbuf, ch->wrctr);
			g_free(fullpath);
		}
	}
	return err;
}

static int cache_getdir(const char *path, fuse_dirh_t h, fuse_dirfil_t filler)
{
	struct fuse_cache_dirhandle ch;
	int err;
	char **dir;
	struct node *node;

	pthread_mutex_lock(&cache.lock);
	node = cache_lookup(path);
	if (node != NULL && node->dir != NULL) {
		time_t now = time(NULL);
		if (node->dir_valid - now >= 0) {
			for(dir = node->dir; *dir != NULL; dir++) 
				filler(h, *dir, 0, node->stat.st_ino);
			pthread_mutex_unlock(&cache.lock);
			return 0;
		}
	}
	pthread_mutex_unlock(&cache.lock);

	ch.path = path;
	ch.h = h;
	ch.filler = filler;
	ch.dir = g_ptr_array_new();
	ch.wrctr = cache_get_write_ctr();
	err = cache.next_oper->cache_getdir(path, &ch, cache_dirfill);
	g_ptr_array_add(ch.dir, NULL);
	dir = (char **) ch.dir->pdata;
	if (!err)
		cache_add_dir(path, dir);
	else
		g_strfreev(dir);
	g_ptr_array_free(ch.dir, FALSE);
	return err;
}

static int cache_unity_dirfill(fuse_cache_dirh_t ch, const char *name,
                               const struct stat *stbuf)
{
	return ch->filler(ch->h, name, 0, stbuf->st_ino);
}

static int cache_unity_getdir(const char *path, fuse_dirh_t h,
                              fuse_dirfil_t filler)
{
	struct fuse_cache_dirhandle ch;
	ch.h = h;
	ch.filler = filler;
	return cache.next_oper->cache_getdir(path, &ch, cache_unity_dirfill);
}

static int cache_mknod(const char *path, mode_t mode, dev_t rdev)
{
	int err = cache.next_oper->oper.mknod(path, mode, rdev);
	if (!err)
		cache_invalidate_dir(path);
	return err;
}

static int cache_mkdir(const char *path, mode_t mode)
{
	int err = cache.next_oper->oper.mkdir(path, mode);
	if (!err)
		cache_invalidate_dir(path);
	return err;
}

static int cache_unlink(const char *path)
{
	int err = cache.next_oper->oper.unlink(path);
	if (!err)
		cache_invalidate_dir(path);
	return err;
}

static int cache_rmdir(const char *path)
{
	int err = cache.next_oper->oper.rmdir(path);
	if (!err)
		cache_invalidate_dir(path);
	return err;
}

static int cache_symlink(const char *from, const char *to)
{
	int err = cache.next_oper->oper.symlink(from, to);
	if (!err)
		cache_invalidate_dir(to);
	return err;
}

static int cache_rename(const char *from, const char *to)
{
	int err = cache.next_oper->oper.rename(from, to);
	if (!err)
		cache_do_rename(from, to);
	return err;
}

static int cache_link(const char *from, const char *to)
{
	int err = cache.next_oper->oper.link(from, to);
	if (!err) {
		cache_invalidate(from);
		cache_invalidate_dir(to);
	}
	return err;
}

static int cache_chmod(const char *path, mode_t mode)
{
	int err = cache.next_oper->oper.chmod(path, mode);
	if (!err)
		cache_invalidate(path);
	return err;
}

static int cache_chown(const char *path, uid_t uid, gid_t gid)
{
	int err = cache.next_oper->oper.chown(path, uid, gid);
	if (!err)
		cache_invalidate(path);
	return err;
}

static int cache_truncate(const char *path, off_t size)
{
	int err = cache.next_oper->oper.truncate(path, size);
	if (!err)
		cache_invalidate(path);
	return err;
}

static int cache_utime(const char *path, struct utimbuf *buf)
{
	int err = cache.next_oper->oper.utime(path, buf);
	if (!err)
		cache_invalidate(path);
	return err;
}

static int cache_write(const char *path, const char *buf, size_t size,
                       off_t offset, struct fuse_file_info *fi)
{
	int res = cache.next_oper->oper.write(path, buf, size, offset, fi);
	if (res >= 0)
		cache_invalidate_write(path);
	return res;
}

#if FUSE_VERSION >= 25
static int cache_create(const char *path, mode_t mode,
                        struct fuse_file_info *fi)
{
	int err = cache.next_oper->oper.create(path, mode, fi);
	if (!err)
		cache_invalidate_dir(path);
	return err;
}

static int cache_ftruncate(const char *path, off_t size,
                           struct fuse_file_info *fi)
{
	int err = cache.next_oper->oper.ftruncate(path, size, fi);
	if (!err)
		cache_invalidate(path);
	return err;
}

static int cache_fgetattr(const char *path, struct stat *stbuf,
                          struct fuse_file_info *fi)
{
	int err = cache_get_attr(path, stbuf);
	if (err) {
		uint64_t wrctr = cache_get_write_ctr();
		err = cache.next_oper->oper.fgetattr(path, stbuf, fi);
		if (!err)
			cache_add_attr(path, stbuf, wrctr);
	}
	return err;
}
#endif

static void cache_unity_fill(struct fuse_cache_operations *oper,
                             struct fuse_operations *cache_oper)
{
#if FUSE_VERSION >= 23
	cache_oper->init        = oper->oper.init;
	cache_oper->destroy     = oper->oper.destroy;
#endif
	cache_oper->getattr     = oper->oper.getattr;
	cache_oper->readlink    = oper->oper.readlink;
	cache_oper->getdir      = cache_unity_getdir;
	cache_oper->mknod       = oper->oper.mknod;
	cache_oper->mkdir       = oper->oper.mkdir;
	cache_oper->symlink     = oper->oper.symlink;
	cache_oper->unlink      = oper->oper.unlink;
	cache_oper->rmdir       = oper->oper.rmdir;
	cache_oper->rename      = oper->oper.rename;
	cache_oper->link        = oper->oper.link;
	cache_oper->chmod       = oper->oper.chmod;
	cache_oper->chown       = oper->oper.chown;
	cache_oper->truncate    = oper->oper.truncate;
	cache_oper->utime       = oper->oper.utime;
	cache_oper->open        = oper->oper.open;
	cache_oper->read        = oper->oper.read;
	cache_oper->write       = oper->oper.write;
	cache_oper->flush       = oper->oper.flush;
	cache_oper->release     = oper->oper.release;
	cache_oper->fsync       = oper->oper.fsync;
	cache_oper->statfs      = oper->oper.statfs;
	cache_oper->setxattr    = oper->oper.setxattr;
	cache_oper->getxattr    = oper->oper.getxattr;
	cache_oper->listxattr   = oper->oper.listxattr;
	cache_oper->removexattr = oper->oper.removexattr;
#if FUSE_VERSION >= 25
	cache_oper->create      = oper->oper.create;
	cache_oper->ftruncate   = oper->oper.ftruncate;
	cache_oper->fgetattr    = oper->oper.fgetattr;
#endif
}

static void cache_fill(struct fuse_cache_operations *oper,
		       struct fuse_operations *cache_oper)
{
	cache_oper->getattr  = oper->oper.getattr ? cache_getattr : NULL;
	cache_oper->readlink = oper->oper.readlink ? cache_readlink : NULL;
	cache_oper->getdir   = oper->cache_getdir ? cache_getdir : NULL;
	cache_oper->mknod    = oper->oper.mknod ? cache_mknod : NULL;
	cache_oper->mkdir    = oper->oper.mkdir ? cache_mkdir : NULL;
	cache_oper->symlink  = oper->oper.symlink ? cache_symlink : NULL;
	cache_oper->unlink   = oper->oper.unlink ? cache_unlink : NULL;
	cache_oper->rmdir    = oper->oper.rmdir ? cache_rmdir : NULL;
	cache_oper->rename   = oper->oper.rename ? cache_rename : NULL;
	cache_oper->link     = oper->oper.link ? cache_link : NULL;
	cache_oper->chmod    = oper->oper.chmod ? cache_chmod : NULL;
	cache_oper->chown    = oper->oper.chown ? cache_chown : NULL;
	cache_oper->truncate = oper->oper.truncate ? cache_truncate : NULL;
	cache_oper->utime    = oper->oper.utime ? cache_utime : NULL;
	cache_oper->write    = oper->oper.write ? cache_write : NULL;
#if FUSE_VERSION >= 25
	cache_oper->create   = oper->oper.create ? cache_create : NULL;
	cache_oper->ftruncate = oper->oper.ftruncate ? cache_ftruncate : NULL;
	cache_oper->fgetattr = oper->oper.fgetattr ? cache_fgetattr : NULL;
#endif

}

struct fuse_operations *cache_init(struct fuse_cache_operations *oper)
{
	static struct fuse_operations cache_oper;
	cache.next_oper = oper;

	cache_unity_fill(oper, &cache_oper);
	if (cache.on) {
		cache_fill(oper, &cache_oper);
		pthread_mutex_init(&cache.lock, NULL);
		cache.table = g_hash_table_new_full(g_str_hash, g_str_equal,
						    g_free, free_node);
		if (cache.table == NULL) {
			fprintf(stderr, "failed to create cache\n");
			return NULL;
		}
	}
	return &cache_oper;
}

void cache_destroy()
{	
	if (cache.on)
		g_hash_table_destroy(cache.table);
}

static const struct fuse_opt cache_opts[] = {
	{ "cache=yes", offsetof(struct cache, on), 1 },
	{ "cache=no", offsetof(struct cache, on), 0 },
	{ "cache_timeout=%u", offsetof(struct cache, stat_timeout), 0 },
	{ "cache_timeout=%u", offsetof(struct cache, dir_timeout), 0 },
	{ "cache_timeout=%u", offsetof(struct cache, link_timeout), 0 },
	{ "cache_stat_timeout=%u", offsetof(struct cache, stat_timeout), 0 },
	{ "cache_dir_timeout=%u", offsetof(struct cache, dir_timeout), 0 },
	{ "cache_link_timeout=%u", offsetof(struct cache, link_timeout), 0 },
	FUSE_OPT_END
};

int cache_parse_options(struct fuse_args *args)
{
	cache.stat_timeout = DEFAULT_CACHE_TIMEOUT;
	cache.dir_timeout = DEFAULT_CACHE_TIMEOUT;
	cache.link_timeout = DEFAULT_CACHE_TIMEOUT;
	cache.on = 1;

	return fuse_opt_parse(args, &cache, cache_opts, NULL);
}
